"""
简易扫雷

仅考虑最简单的情况，即9*9网格(cell)，初始雷(mine)的数量10颗。
随机进行埋雷。

顶部设置提示栏，表示剩余雷的数量(rest count)，以及当前状态(status)。
及重新开始（restart）。

对于每个非雷区域(safe zone)，计算其周遭雷的数量(hint number)。


(auto reveal)如果点击的区域，周围均无雷，则自动翻开周围的区域，以此类推。（此处按照广度优先算法展开）
TODO (First Click Safety)保证第一次点击方格没有埋雷。最后再处理。


事件：
1. 左键触发：挖开区域(reveal)
2. 右键触发：标记(flag)或取消(cancel flag)标记雷
3. 滚轮点击触发：如果周围标记雷的数量与当前区域显示雷的数量相等，则挖开周遭未挖开的区域(auto reveal)。


结束条件：
1. 将雷挖出来；
2. 除了标记雷的区域，均被挖开，且标记雷的数量与初始雷的数量相等。

"""
from __future__ import annotations

import math
import random
import time
from typing import List

import arcade


"""
区域坐标约定。
每个雷区大小为30*30；
总雷区大小为270*270
导航条高度暂定为50

则窗口大小为270*320
雷区坐标位置是：(0,0)~(270,270)

"""
SCREEN_WIDTH=270
SCREEN_HEIGHT=320
MATRIX_WIDTH=9
MATRIX_HEIGHT=9
MINE_COUNT=10

# 采用颜色
COLOR_BACKGROUND=(240,240,240)
COLOR_UNREVEAL=(192,192,192)
COLOR_REVEAL=(255,255,255)
COLOR_1=(0,0,255)
COLOR_2=(0,128,0)
COLOR_3=(255,0,0)
COLOR_4=(0,0,128)
COLOR_5=(128,0,0)
COLOR_6_8=(128,0,0)

COLOR_NUMBER_DICT={
    1:COLOR_1,
    2:COLOR_2,
    3:COLOR_3,
    4:COLOR_4,
    5:COLOR_5,
    6:COLOR_6_8,
    7:COLOR_6_8,
    8:COLOR_6_8
}

def reset_rand_mine(mine_cells:List[List[int]],mine_size):
    # 随机放雷
    height=len(mine_cells)
    width=len(mine_cells[0])
    cell_count=height*width

    for ridx in range(MATRIX_HEIGHT):
        for cidx in range(MATRIX_WIDTH):
            mine_cells[ridx][cidx]=0

    rand_nums=random.sample(range(cell_count), mine_size)
    for num in rand_nums:
        print(f'num={num}')
        ridx=num//width
        cidx=num%width
        # print(f'r={ridx},c={cidx}')
        mine_cells[ridx][cidx]=1
        

def incr_matrix_if_possible(matrix:List[List[int]],ridx:int,cidx:int)->None:
    # 在可能的情况加增加矩阵数字
    if 0<=ridx<MATRIX_HEIGHT and 0<=cidx<MATRIX_WIDTH:
        matrix[ridx][cidx]+=1
            

def calc_clue_by_mine(clue_cells,mine_cells):
    # 根据真实雷的位置，计算线索数
    for ridx in range(MATRIX_HEIGHT):
        for cidx in range(MATRIX_WIDTH):
            clue_cells[ridx][cidx]=0
    for ridx in range(MATRIX_HEIGHT):
        for cidx in range(MATRIX_WIDTH):
            if mine_cells[ridx][cidx]==1:
                incr_matrix_if_possible(clue_cells,ridx-1,cidx-1)
                incr_matrix_if_possible(clue_cells,ridx-1,cidx)
                incr_matrix_if_possible(clue_cells,ridx-1,cidx+1)
                incr_matrix_if_possible(clue_cells,ridx,cidx-1)
                incr_matrix_if_possible(clue_cells,ridx,cidx+1)
                incr_matrix_if_possible(clue_cells,ridx+1,cidx-1)
                incr_matrix_if_possible(clue_cells,ridx+1,cidx)
                incr_matrix_if_possible(clue_cells,ridx+1,cidx+1)

    pass


class GridParser:
    """
    网格计算解析器
    """

    def __init__(self,row_count,col_count,start_x,start_y,end_x,end_y):
        self.row_count=row_count
        self.col_count=col_count

        self.min_x=min(start_x,end_x)
        self.min_y=min(start_y,end_y)

        self.max_x=max(start_x,end_x)
        self.max_y=max(start_y,end_y)

        wid_x=self.max_x-self.min_x
        hei_y=self.max_y-self.min_y
        

        self.cell_x=wid_x/col_count
        self.cell_y=hei_y/row_count
        pass
    

    def calc_xy_by_rc(self,r,c):
        # 根据rc计算单元格的中心坐标
        x=int(self.min_x+self.cell_x*c+self.cell_x/2)
        y=int(self.min_y+self.cell_y*(self.row_count-r-1)+self.cell_y/2)
        
        return (x,y)
    
    def calc_rc_by_xy(self,x,y):
        # 根据xy坐标计算rc的位置
        if x<self.min_x or x>self.max_x:
            return None
        if y<self.min_y or y>self.max_y:
            return None

        col=(x-self.min_x)//self.cell_x
        row=self.row_count-((y-self.min_y)//self.cell_y)-1

        return (int(row),int(col))

# 栅格计算器
gridParser=GridParser(9,9,0,0,270,270)

class MineBoard:
    def __init__(self):
        # 初始化基础参数
        self.width=9
        self.height=9
        self.max_mine_count=MINE_COUNT

        # 结束标记。0表示未结束；1表示成功排完雷且结束；2表示排到雷所以结束
        self.end_mark=0

        # 初始化雷板(0表示无雷，1表示有雷)
        self.mine_cells=[[0]*self.width for _ in range(self.height)]

        # 初始化线索区(标记周遭雷的数量)
        self.clue_cells=[[0]*self.width for _ in range(self.height)]

        # 初始化揭示区：0表示未揭示，1表示已安全揭示，2表示已标记，3表示已揭示且为雷
        self.reveal_cells=[[0]*self.width for _ in range(self.height)]

        pass

    def reset(self):
        """重置雷区"""
        # 雷清空后，再随机放置
        reset_rand_mine(self.mine_cells,self.max_mine_count)
        
        # 线索数清空后，最根据雷的位置计算
        calc_clue_by_mine(self.clue_cells,self.mine_cells)

        # 清空揭示区
        for ridx in range(self.height):
            for cidx in range(self.width):
                self.reveal_cells[ridx][cidx]=0

        # 重置各项标记
        self.flag_count=0
        self.end_mark=0

        pass

    def offset_safe(self):
        # TODO 安全偏移（最后再考虑）
        pass

    def reveal(self,ridx,cidx):
        # 揭示雷区
        if self.mine_cells[ridx][cidx]==1:
            self.end_mark=2
            self.reveal_cells[ridx][cidx]=3
        else:
            self.reveal_cells[ridx][cidx]=1
            if self.clue_cells[ridx][cidx]==0:
                self.reveal_zero(ridx,cidx)

        pass

    def flag(self,ridx,cidx):
        if self.reveal_cells[ridx][cidx]==1:
            return
        # 标记或取消标记
        if self.reveal_cells[ridx][cidx]==0:
            self.reveal_cells[ridx][cidx]=2
            self.flag_count+=1
        else:
            self.reveal_cells[ridx][cidx]=0     
            self.flag_count-=1

        pass
        

    def reveal_round(self,ridx,cidx):
        # 自动揭示
        if self.reveal_cells[ridx][cidx]!=1:
            return

        # 判断：当揭示数字与周围flag数量相等时，揭示所有未标记的cell
        flag_count=0
        for ri in range(max(0,ridx-1),min(ridx+1,self.height-1)+1):
            for ci in range(max(cidx-1,0),min(cidx+1,self.width-1)+1):
                if self.reveal_cells[ri][ci]==2:
                    flag_count+=1
        if flag_count!=self.clue_cells[ridx][cidx]:
            return
        for ri in range(max(0,ridx-1),min(ridx+1,self.height-1)+1):
            for ci in range(max(cidx-1,0),min(cidx+1,self.width-1)+1):
                if self.reveal_cells[ri][ci]==0:
                    self.reveal(ri,ci)

        pass
    
    def reveal_zero(self,ridx,cidx):
        # 揭示线索0（广度优先探索）
        # 先确保当前位置线索数为0
        # if not (0<=ridx<self.height and 0<=cidx<self.width):
        #     return 
        # 揭示操作
        # self.reveal_cells[ridx][cidx]=1

        # 如果线索数是0，就此打住
        if self.clue_cells[ridx][cidx]!=0:
            return

        # 如果线索数不是0，向上下左右8个方向，递归揭示
        zero_to_reveal=[]
        for ri in range(max(ridx-1,0),min(ridx+1,self.height-1)+1):
            for ci in range(max(cidx-1,0),min(cidx+1,self.width-1)+1):
                if self.reveal_cells[ri][ci]==0:
                    self.reveal_cells[ri][ci]=1
                    # print(f'reveal:{ri},{ci},{self.clue_cells[ri][ci]}')
                    # 不能使用sleep，会导致异常
                    # time.sleep(1)
                    if self.clue_cells[ri][ci]==0:
                        # self.reveal_zero(ri,ci)
                        zero_to_reveal.append((ri,ci))
        for (ri,ci) in zero_to_reveal:
            self.reveal_zero(ri,ci)
        pass

    def reveal_other_mine(self):
        # 揭示其他雷。用于失败后的处理。
        pass

    def judge_success(self)->None:
        # 判断是否胜利
        if self.end_mark!=0:
            return
        if self.flag_count!=self.max_mine_count:
            return
        for ridx in range(self.height):
            for cidx in range(self.width):
                if self.reveal_cells[ridx][cidx]==0:
                    return
        self.end_mark=1


class MineBoardUtil:

    @staticmethod
    def draw_board():
        # 画基础雷盘
        arcade.draw_lbwh_rectangle_filled(0,0,270,270,arcade.csscolor.GRAY)
        for i in range(1,10):
            # 横线(9条)
            arcade.draw_line(0,30*i,270,30*i,arcade.color.BLACK)
        for i in range(1,9):
            # 纵线（8条）
            arcade.draw_line(30*i,0,30*i,270,arcade.color.BLACK)
        pass
    
    @staticmethod
    def draw_reveal(r,c):
        # 画揭示区域
        x,y=gridParser.calc_xy_by_rc(r,c)
        arcade.draw_lbwh_rectangle_filled(x-15,y-15,30,30,COLOR_REVEAL)
        pass

    @staticmethod
    def draw_mine(r,c):
        # 画雷（使用半径为10的蓝色圆表示）
        x,y=gridParser.calc_xy_by_rc(r,c)
        arcade.draw_circle_filled(x,y,10,arcade.color.BLUE)
        pass
    @staticmethod
    def draw_flag(r,c):
        # 画标记（使用红色20*10的方块+高度为20的竖线，表示小红旗）
        x,y=gridParser.calc_xy_by_rc(r,c)
        arcade.draw_lbwh_rectangle_filled(x-10,y,20,10,arcade.color.RED)
        arcade.draw_line(x-10,y+10,x-10,y-10,arcade.color.BLACK)

        pass

    @staticmethod
    def draw_clue(r,c,num):
        if num==0:
            return
        # 画线索数字（使用font-size=12的数字表示）
        x,y=gridParser.calc_xy_by_rc(r,c)
        arcade.draw_text(
            str(num),
            x,
            y,
            COLOR_NUMBER_DICT[num],
            anchor_x="center",
            anchor_y="center",
            font_name=("Arial", "Microsoft YaHei"))
        pass

    @staticmethod
    def draw_nav_bar(mine_count,end_mark):
        # 导航条，暂时只记录剩余雷的数量(270~320)和结束标记
        arcade.draw_text(
            str(mine_count),
            270//2,
            (270+320)//2,
            arcade.color.RED,
            font_size=16,
            anchor_x='center',
            anchor_y='center',
            font_name=("Arial", "Microsoft YaHei")
            )
        
        end_info=''
        if end_mark==0:
            end_info='进行中'
        elif end_mark==1:
            end_info='成功'
        else:
            end_info='失败'
        arcade.draw_text(
            str(end_info),
            50,
            (270+320)//2,
            arcade.color.RED,
            font_size=16,
            anchor_x='center',
            anchor_y='center',
            font_name=("Arial", "Microsoft YaHei")
        )
        pass

    pass

    def draw_reset_buttton():
        # 重置按钮（）
        arcade.draw_lrbt_rectangle_filled()



class MineBoardView(arcade.View):
    def __init__(self):
        # 继承类初始化时，必须调用父类的__init__方法
        super().__init__(background_color=COLOR_BACKGROUND)
        # 初始化雷版信息
        self.mb=MineBoard()
        self.window.set_caption('minesweeper')
        pass

    def on_show_view(self):
        # 重置雷板信息
        self.mb.reset()
        pass

    def on_update(self, delta_time):
        # 计算视图坐标信息(不需要频繁更新图像的情况下，此处可以不处理)
        pass

    def on_draw(self):
        # 通用逻辑
        self.clear()
        # 画棋盘
        MineBoardUtil.draw_board()

        # 画cell内部
        for ridx in range(self.mb.height):
            for cidx in range(self.mb.width):
                reveal_val=self.mb.reveal_cells[ridx][cidx]
                clue_val=self.mb.clue_cells[ridx][cidx]
                if reveal_val==1:
                    MineBoardUtil.draw_reveal(ridx,cidx)
                    # 画线索
                    if clue_val>0:
                        MineBoardUtil.draw_clue(ridx,cidx,clue_val)
                elif reveal_val==2:
                    # 画标记
                    MineBoardUtil.draw_flag(ridx,cidx)
                elif reveal_val==3:
                    # 画雷
                    MineBoardUtil.draw_reveal(ridx,cidx)
                    MineBoardUtil.draw_mine(ridx,cidx)

    
        # 画上方区域
        MineBoardUtil.draw_nav_bar(self.mb.max_mine_count-self.mb.flag_count,self.mb.end_mark)

        pass

    def on_mouse_press(self, x, y, button, modifiers):
        # 仅当没有结束才可以继续点击
        if self.mb.end_mark!=0:
            return

        # 根据x,y计算雷版单元格索引
        parse_res=gridParser.calc_rc_by_xy(x,y)
        # print(f'相应:{parse_res}')
        if not parse_res:
            return

        r,c=parse_res
        # 根据鼠标类型不同，触发不同事件
        if button==arcade.MOUSE_BUTTON_LEFT:
            self.mb.reveal(r,c)
        elif button==arcade.MOUSE_BUTTON_RIGHT:
            self.mb.flag(r,c)
        elif button==arcade.MOUSE_BUTTON_MIDDLE:
            self.mb.reveal_round(r,c)

        # 每次点击后判断是否胜利（失败逻辑在揭示中体现）
        self.mb.judge_success()

        pass


def main():
    window=arcade.Window(
        width=SCREEN_WIDTH,
        height=SCREEN_HEIGHT,
        resizable=False
    )
    # window.set_background_color(COLOR_BACKGROUND)
    window.show_view(MineBoardView())

    arcade.run()


if __name__=='__main__':
    main()
